﻿using System;
using System.Collections.Generic;
using System.Linq;
using ECommerce.Web.FetchingStrategies;
using NHibernate;
using NHibernate.Criterion;
using NHibernate.Engine;
using NHibernate.Metadata;
using StructureMap;

namespace ECommerce.Web.NHibernate.Sessions
{
    public static class SessionExtensions
    {
        public static T FindById<T>(this ISession session, object id) where T : class
        {
            T entity;
            
            Type persistentType = GetPersistentType(session.SessionFactory, typeof(T));

            var fetchingStrategies = ObjectFactory.GetAllInstances<IFetchingStrategy<T>>();

            if (fetchingStrategies.Any())
            {
                ICriteria criteria = CreateCriteriaForPersistentType(session, persistentType, id);

                foreach (var fetchingStrategy in fetchingStrategies)
                {
                    fetchingStrategy.AddFetchJoinTo(criteria);
                }

                entity = criteria.UniqueResult<T>();
            }
            else
            {
                entity = session.Get<T>(id);
            }

            if (entity == null)
            {
                if (TypeHasEmptyConstructor(persistentType))
                {
                    entity = (T)CreateInstanceOfPersistentType(session, persistentType, id);
                }
                else
                {
                    return null;
                }
            }

            session.SaveOrUpdate(entity);

            return entity;
        }

        private static Type GetPersistentType(ISessionFactory sessionFactory, Type type)
        {
            if (!type.IsAbstract)
            {
                return type;
            }

            IList<Type> persistentTypes = new List<Type>();

            foreach (KeyValuePair<string, IClassMetadata> classMetadata in sessionFactory.GetAllClassMetadata())
            {
                Type mappedClass = classMetadata.Value.GetMappedClass(EntityMode.Poco);

                if (type.IsAssignableFrom(mappedClass))
                {
                    persistentTypes.Add(mappedClass);
                }
            }

            return persistentTypes.Except(persistentTypes.Where(x => BaseTypeIsInList(persistentTypes, x))).FirstOrDefault() ?? type;
        }

        private static bool BaseTypeIsInList(IEnumerable<Type> types, Type type)
        {
            return types.Any(x => x != type && x.IsAssignableFrom(type));
        }

        private static ICriteria CreateCriteriaForPersistentType(ISession session, Type persistentType, object id)
        {
            string idPropertyName = GetIdPropertyNameForPersistentType(session.SessionFactory, persistentType);

            var criteria = session.CreateCriteria(persistentType);

            criteria.Add(Expression.Eq(idPropertyName, id));

            return criteria;
        }

        private static string GetIdPropertyNameForPersistentType(ISessionFactory sessionFactory, Type persistentType)
        {
            IClassMetadata classMetadata = sessionFactory.GetClassMetadata(persistentType);

            return classMetadata.IdentifierPropertyName;
        }

        private static bool TypeHasEmptyConstructor(Type type)
        {
            return type.GetConstructor(new Type[] { }) != null;
        }

        private static object CreateInstanceOfPersistentType(ISession session, Type persistentType, object id)
        {
            ISessionImplementor sessionImplementor = session.GetSessionImplementation();

            return sessionImplementor.Instantiate(persistentType.FullName, id);
        }
    }
}
